/*
 * $Id: GarbageFreeOutputStreamWriter.java 118 2013-01-17 10:31:00Z vladimirow@mail.ru $
 * $HeadURL: http://ultralog.googlecode.com/svn/ultralog/trunk/src/java/com/google/code/ultralog/writer/GarbageFreeOutputStreamWriter.java $
 *
 * This file is a part of ultralog project (http://code.google.com/p/ultralog/).
 * Released under New BSD License.
 */
package com.google.code.ultralog.writer;

import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;

/**
 * Subclass of {@link Writer} that writes data into given {@link OutputStream}
 * without producing garbage.
 *
 * @author Mikhail Vladimirov
 */
public class GarbageFreeOutputStreamWriter extends Writer
{
    private final OutputStream output;
    private final CharsetEncoder charsetEncoder;

    private final char [] chars;
    private final byte [] bytes;

    private final CharBuffer charBuffer;
    private final ByteBuffer byteBuffer;

    /**
     * Create new writer that writes data into given {@link OutputStream}
     * using default character set and with capacity of 65536 characters.
     *
     * @param output {@link OutputStream} to write data into
     */
    public GarbageFreeOutputStreamWriter (
        OutputStream output)
    {
        this (output, Charset.defaultCharset ().newEncoder (), 65536);
    }

    /**
     * Create new writer that writes data into given {@link OutputStream}
     * using given {@link CharsetEncoder} and with capacity of 65536 characters.
     *
     * @param output {@link OutputStream} to write data into
     * @param charsetEncoder {@link CharsetEncoder} to use
     */
    public GarbageFreeOutputStreamWriter (
        OutputStream output,
        CharsetEncoder charsetEncoder)
    {
        this (output, charsetEncoder, 65536);
    }

    /**
     * Create new writer that writes data into given {@link OutputStream}
     * using default character set and with given capacity.
     *
     * @param output {@link OutputStream} to write data into
     * @param capacity buffer capacity
     */
    public GarbageFreeOutputStreamWriter (
        OutputStream output,
        int capacity)
    {
        this (output, Charset.defaultCharset ().newEncoder (), capacity);
    }

    /**
     * Create new writer that writes data into given {@link OutputStream}
     * using given {@link CharsetEncoder} and with given capacity.
     *
     * @param output {@link OutputStream} to write data into
     * @param charsetEncoder {@link CharsetEncoder} to use
     * @param capacity buffer capacity
     */
    public GarbageFreeOutputStreamWriter (
        OutputStream output,
        CharsetEncoder charsetEncoder,
        int capacity)
    {
        if (output == null)
            throw new IllegalArgumentException ("Output is null");

        if (charsetEncoder == null)
            throw new IllegalArgumentException ("Charset encoder is null");

        if (capacity <= 0)
            throw new IllegalArgumentException (
                "Capacity (" + capacity + ")<= 0");

        this.output = output;
        this.charsetEncoder = charsetEncoder;

        chars = new char [capacity];
        bytes =
            new byte [
                (int)Math.ceil (capacity * charsetEncoder.maxBytesPerChar ())];

        charBuffer = CharBuffer.wrap (chars);
        byteBuffer = ByteBuffer.wrap (bytes);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void write (int c) throws IOException
    {
        if (charBuffer.remaining () < 1)
            drain (false);

        charBuffer.put ((char)c);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void write (char[] cbuf) throws IOException
    {
        if (cbuf == null)
            throw new IllegalArgumentException ("Cbuf is null");

        write (cbuf, 0, cbuf.length);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void write (char[] cbuf, int off, int len) throws IOException
    {
        if (cbuf == null)
            throw new IllegalArgumentException ("Cbuf is null");

        if (off < 0)
            throw new IllegalArgumentException ("Off (" + off + ") < 0");

        if (len < 0)
            throw new IllegalArgumentException ("Len (" + len + ") < 0");

        int cbufLength = cbuf.length;

        if (off + len > cbufLength)
            throw new IllegalArgumentException (
                "Off (" + off + ") + len (" + len + ") > cbuf length (" +
                cbufLength + ")");

        while (len > 0)
        {
            int c = Math.min (len, charBuffer.remaining ());

            if (c == 0)
                drain (false);
            else
            {
                charBuffer.put (cbuf, off, c);
                off += c;
                len -= c;
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void write (String str) throws IOException
    {
        if (str == null)
            throw new IllegalArgumentException ("Str is null");

        write (str, 0, str.length ());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void write (String str, int off, int len) throws IOException
    {
        if (str == null)
            throw new IllegalArgumentException ("Str is null");

        if (off < 0)
            throw new IllegalArgumentException ("Off (" + off + ") < 0");

        if (len < 0)
            throw new IllegalArgumentException ("Len (" + len + ") < 0");

        int strLength = str.length ();

        if (off + len > strLength)
            throw new IllegalArgumentException (
                "Off (" + off + ") + len (" + len + ") > str length (" +
                        strLength + ")");

        while (len > 0)
        {
            int c = Math.min (len, charBuffer.remaining ());

            if (c == 0)
                drain (false);
            else
            {
                charBuffer.put (str, off, off + c);
                off += c;
                len -= c;
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Writer append (CharSequence csq) throws IOException
    {
        if (csq == null)
            throw new IllegalArgumentException ("Csq is null");

        return append (csq, 0, csq.length ());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Writer append (CharSequence csq, int start, int end)
        throws IOException
    {
        if (csq == null)
            throw new IllegalArgumentException ("Str is null");

        if (start < 0)
            throw new IllegalArgumentException ("Start (" + start + ") < 0");

        if (end < start)
            throw new IllegalArgumentException (
                "End (" + end + ") < start (" + start + ")");

        int csqLength = csq.length ();

        if (end > csqLength)
            throw new IllegalArgumentException (
                "End (" + end + ") > csq length (" +
                        csqLength + ")");

        int off = start;
        int len = end - start;

        while (len > 0)
        {
            int c = Math.min (len, charBuffer.remaining ());

            if (c == 0)
                drain (false);
            else
            {
                for (int i = 0, j = off; i < c; i++, j++)
                    charBuffer.put (csq.charAt (j));
                off += c;
                len -= c;
            }
        }

        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Writer append (char c) throws IOException
    {
        write (c);

        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void flush () throws IOException
    {
        drain (true);
        output.flush ();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void close () throws IOException
    {
        drain (true);
        output.close ();
    }

    private void drain (boolean endOfInput) throws IOException
    {
        charBuffer.flip ();
        byteBuffer.clear ();

        CoderResult result = charsetEncoder.encode (
            charBuffer, byteBuffer, endOfInput);

        if (CoderResult.OVERFLOW.equals (result))
            throw new IOException ("Overflow while encoding characters");

        output.write (bytes, 0, byteBuffer.position ());

        charBuffer.clear ();
    }
}
